home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 10869 / 10869.xpi / components / jsonview.js
Text File  |  2009-10-08  |  13KB  |  341 lines

  1. /**
  2.  * @author Benjamin Hollis
  3.  * 
  4.  * This component provides a stream converter that can translate from JSON to HTML.
  5.  * It is compatible with Firefox 3 and up, since it uses many components that are new
  6.  * to Firefox 3.
  7.  */
  8.  
  9. // Save some tedious typing
  10. const Ci = Components.interfaces;
  11. const Cc = Components.classes;
  12.  
  13. // Import XPCOMUtils to help set up our JSONView XPCOM component (new to FF3)
  14. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  15.  
  16. // Let us use FUEL
  17. var Application = Cc["@mozilla.org/fuel/application;1"].getService(Ci.fuelIApplication);
  18.  
  19.  
  20. /* 
  21.  * The JSONFormatter helper object. This contains two major functions, jsonToHTML and errorPage, 
  22.  * each of which returns an HTML document.
  23.  */ 
  24. function JSONFormatter() {
  25.   var src = 'chrome://jsonview/locale/jsonview.properties';
  26.   var localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService);
  27.  
  28.   var appLocale = localeService.getApplicationLocale();
  29.   var stringBundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
  30.   this.stringbundle = stringBundleService.createBundle(src, appLocale);
  31. }
  32. JSONFormatter.prototype = {
  33.   htmlEncode: function (t) {
  34.     return t != null ? t.toString().replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">") : '';
  35.   },
  36.   
  37.   decorateWithSpan: function (value, className) {
  38.     return '<span class="' + className + '">' + this.htmlEncode(value) + '</span>';
  39.   },
  40.   
  41.   // Convert a basic JSON datatype (number, string, boolean, null, object, array) into an HTML fragment.
  42.   valueToHTML: function(value) {
  43.     var valueType = typeof value;
  44.     
  45.     var output = "";
  46.     if (value == null) {
  47.       output += this.decorateWithSpan('null', 'null');
  48.     }
  49.     else if (value && value.constructor == Array) {
  50.       output += this.arrayToHTML(value);
  51.     }
  52.     else if (valueType == 'object') {
  53.       output += this.objectToHTML(value);
  54.     } 
  55.     else if (valueType == 'number') {
  56.       output += this.decorateWithSpan(value, 'num');
  57.     }
  58.     else if (valueType == 'string') {
  59.       if (/^(http|https):\/\/[^\s]+$/.test(value)) {
  60.         output += '<a href="' + value + '">' + this.htmlEncode(value) + '</a>';
  61.       } else {
  62.         output += this.decorateWithSpan('"' + value + '"', 'string');
  63.       }
  64.     }
  65.     else if (valueType == 'boolean') {
  66.       output += this.decorateWithSpan(value, 'bool');
  67.     }
  68.     
  69.     return output;
  70.   },
  71.   
  72.   // Convert an array into an HTML fragment
  73.   arrayToHTML: function(json) {
  74.     var output = '[<ul class="array collapsible">';
  75.     var hasContents = false;
  76.     for ( var prop in json ) {
  77.       hasContents = true;
  78.       output += '<li>';
  79.       output += this.valueToHTML(json[prop]);
  80.       output += '</li>';
  81.     }
  82.     output += '</ul>]';
  83.     
  84.     if ( ! hasContents ) {
  85.       output = "[ ]";
  86.     }
  87.     
  88.     return output;
  89.   },
  90.   
  91.   // Convert a JSON object to an HTML fragment
  92.   objectToHTML: function(json) {
  93.     var output = '{<ul class="obj collapsible">';
  94.     var hasContents = false;
  95.     for ( var prop in json ) {
  96.       hasContents = true;
  97.       output += '<li>';
  98.       output += '<span class="prop">' + this.htmlEncode(prop) + '</span>: '
  99.       output += this.valueToHTML(json[prop]);
  100.       output += '</li>';
  101.     }
  102.     output += '</ul>}';
  103.     
  104.     if ( ! hasContents ) {
  105.       output = "{ }";
  106.     }
  107.     
  108.     return output;
  109.   },
  110.   
  111.   // Convert a whole JSON object into a formatted HTML document.
  112.   jsonToHTML: function(json, callback, uri) {
  113.     var output = '';
  114.     if( callback ){
  115.       output += '<div class="callback">' + callback + ' (</div>';
  116.       output += '<div id="json">';
  117.     }else{
  118.       output += '<div id="json">';
  119.     }
  120.     output += this.valueToHTML(json);
  121.     output += '</div>';
  122.     if( callback ){
  123.       output += '<div class="callback">)</div>';
  124.     }
  125.     return this.toHTML(output, uri);
  126.   },
  127.   
  128.   // Produce an error document for when parsing fails.
  129.   errorPage: function(error, data, uri) {
  130.     var output = '<div id="error">' + this.stringbundle.GetStringFromName('errorParsing') + '</div>';
  131.     output += '<h1>' + this.stringbundle.GetStringFromName('docContents') + ':</h1>';
  132.     output += '<div id="json">' + this.htmlEncode(data) + '</div>';
  133.     return this.toHTML(output, uri + ' - Error');
  134.   },
  135.   
  136.   // Wrap the HTML fragment in a full document. Used by jsonToHTML and errorPage.
  137.   toHTML: function(content, title) {
  138.     return '<doctype html>' + 
  139.       '<html><head><title>' + title + '</title>' +
  140.       '<link rel="stylesheet" type="text/css" href="chrome://jsonview/content/default.css">' + 
  141.       '<script type="text/javascript" src="chrome://jsonview/content/default.js"></script>' + 
  142.       '</head><body>' +
  143.       content + 
  144.       '</body></html>';
  145.   }
  146. };
  147.  
  148. // This component is an implementation of nsIStreamConverter that converts application/json to html
  149. const JSONVIEW_CONVERSION =
  150.     "?from=application/json&to=*/*";
  151. const JSONVIEW_CONTRACT_ID =
  152.     "@mozilla.org/streamconv;1" + JSONVIEW_CONVERSION;
  153. const JSONVIEW_COMPONENT_ID = 
  154.     Components.ID("{64890660-53c4-11dd-ae16-0800200c9a66}");
  155.     
  156. // JSONView class constructor. Not much to see here.
  157. function JSONView() {  
  158.   // Native JSON implementation, new to FF3
  159.   this.nativeJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  160.   this.jsonFormatter = new JSONFormatter();
  161. };
  162.  
  163. // This defines an object that implements our converter, and is set up to be XPCOM-ified by XPCOMUtils
  164. JSONView.prototype = {
  165.  
  166.   // properties required for XPCOM registration:
  167.   classDescription: "JSONView XPCOM Component",
  168.   classID:          JSONVIEW_COMPONENT_ID,
  169.   contractID:       JSONVIEW_CONTRACT_ID,
  170.  
  171.   _xpcom_categories: [{
  172.     category: "@mozilla.org/streamconv;1",
  173.     entry: JSONVIEW_CONVERSION,
  174.     value: "JSON to HTML stream converter"
  175.   }],
  176.   
  177.   QueryInterface: XPCOMUtils.generateQI([
  178.       Ci.nsISupports,
  179.       Ci.nsIStreamConverter,
  180.       Ci.nsIStreamListener,
  181.       Ci.nsIRequestObserver
  182.   ]),
  183.   
  184.   /*
  185.    * This component works as such:
  186.    * 1. asyncConvertData captures the listener
  187.    * 2. onStartRequest fires, initializes stuff, modifies the listener to match our output type
  188.    * 3. onDataAvailable transcodes the data into a UTF-8 string
  189.    * 4. onStopRequest gets the collected data and converts it, spits it to the listener
  190.    * 5. convert does nothing, it's just the synchronous version of asyncConvertData
  191.    */
  192.   
  193.   // nsIStreamConverter::convert
  194.   convert: function (aFromStream, aFromType, aToType, aCtxt) {
  195.       return aFromStream;
  196.   },
  197.   
  198.   // nsIStreamConverter::asyncConvertData
  199.   asyncConvertData: function (aFromType, aToType, aListener, aCtxt) {
  200.     // Store the listener passed to us
  201.     this.listener = aListener;
  202.   },
  203.   
  204.   // nsIStreamListener::onDataAvailable
  205.   onDataAvailable: function (aRequest, aContext, aInputStream, aOffset, aCount) {
  206.     // From https://developer.mozilla.org/en/Reading_textual_data
  207.     var is = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
  208.     is.init(aInputStream, this.charset, -1, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  209.   
  210.     var str = {};
  211.     
  212.     // This used to read in a loop until readString returned 0, but it caused it to crash Firefox on OSX/Win32 (but not Win64)
  213.     // It seems just reading once with -1 (default buffer size) gets the file done.
  214.     // However, *not* reading in a loop seems to cause problems with Firebug
  215.     // So I read in a loop, but do whatever I can to avoid infinite-looping.
  216.     var totalBytesRead = 0;
  217.     var bytesRead = 1; // Seed it with something positive
  218.     
  219.     while (totalBytesRead < aCount && bytesRead > 0) {
  220.       bytesRead = is.readString(-1, str);
  221.       totalBytesRead += bytesRead;
  222.       this.data += str.value;
  223.     }
  224.     
  225.   },
  226.   
  227.   // nsIRequestObserver::onStartRequest
  228.   onStartRequest: function (aRequest, aContext) {
  229.     this.data = '';
  230.     this.uri = aRequest.QueryInterface(Ci.nsIChannel).URI.spec;
  231.  
  232.     // Sets the charset if it is available. (For documents loaded from the
  233.     // filesystem, this is not set.)
  234.     this.charset = aRequest.QueryInterface(Ci.nsIChannel).contentCharset || 'UTF-8';
  235.  
  236.     this.channel = aRequest;
  237.     this.channel.contentType = "text/html";
  238.     // All our data will be coerced to UTF-8
  239.     this.channel.contentCharset = "UTF-8";
  240.  
  241.     this.listener.onStartRequest (this.channel, aContext);
  242.   },
  243.   
  244.   // nsIRequestObserver::onStopRequest
  245.   onStopRequest: function (aRequest, aContext, aStatusCode) {
  246.     /*
  247.      * This should go something like this:
  248.      * 1. Make sure we have a unicode string.
  249.      * 2. Convert it to a Javascript object.
  250.      * 2.1 Removes the callback
  251.      * 3. Convert that to HTML? Or XUL?
  252.      * 4. Spit it back out at the listener
  253.      */
  254.     
  255.     var converter = Components
  256.         .classes["@mozilla.org/intl/scriptableunicodeconverter"]
  257.         .createInstance(Ci.nsIScriptableUnicodeConverter);
  258.     converter.charset = this.charset;
  259.     var outputDoc = "";
  260.     
  261.     var cleanData = '',
  262.         callback = '';
  263.  
  264.     // This regex attempts to match a JSONP structure:
  265.     //    * Any amount of whitespace (including unicode nonbreaking spaces) between the start of the file and the callback name
  266.     //    * Callback name (any valid JavaScript function name according to ECMA-262 Edition 3 spec)
  267.     //    * Any amount of whitespace (including unicode nonbreaking spaces)
  268.     //    * Open parentheses
  269.     //    * Any amount of whitespace (including unicode nonbreaking spaces)
  270.     //    * Either { or [, the only two valid characters to start a JSON string.
  271.     //    * Any character, any number of times
  272.     //    * Either } or ], the only two valid closing characters of a JSON string.
  273.     //    * Any amount of whitespace (including unicode nonbreaking spaces)
  274.     //    * A closing parenthesis, an optional semicolon, and any amount of whitespace (including unicode nonbreaking spaces) until the end of the file.
  275.     // This will miss anything that has comments, or more than one callback, or requires modification before use.
  276.     var callback_results = /^[\s\u200B\uFEFF]*([\w$\[\]\.]+)[\s\u200B\uFEFF]*\([\s\u200B\uFEFF]*([\[{][\s\S]*[\]}])[\s\u200B\uFEFF]*\);?[\s\u200B\uFEFF]*$/.exec(this.data);
  277.     if( callback_results && callback_results.length == 3 ){
  278.       callback = callback_results[1];
  279.       cleanData = callback_results[2];
  280.     }else{
  281.       cleanData = this.data;
  282.     }
  283.     
  284.     try {
  285.       var jsonObj = this.nativeJSON.decode(cleanData);
  286.       if ( jsonObj ) {        
  287.         outputDoc = this.jsonFormatter.jsonToHTML(jsonObj, callback, this.uri);
  288.       } else {
  289.         throw "There was no object!";
  290.       }
  291.     }
  292.     catch(e) {
  293.       outputDoc = this.jsonFormatter.errorPage(e, this.data, this.uri);
  294.     }
  295.     
  296.     // I don't really understand this part, but basically it's a way to get our UTF-8 stuff
  297.     // spit back out as a byte stream
  298.     // See http://www.mail-archive.com/mozilla-xpcom@mozilla.org/msg04194.html
  299.     var storage = Cc["@mozilla.org/storagestream;1"]
  300.     .createInstance(Ci.nsIStorageStream);
  301.     
  302.     // I have no idea what to pick for the first parameter (segments)
  303.     storage.init(4, 0xffffffff, null);
  304.     var out = storage.getOutputStream(0);
  305.     
  306.     var binout = Cc["@mozilla.org/binaryoutputstream;1"]
  307.     .createInstance(Ci.nsIBinaryOutputStream);
  308.     binout.setOutputStream(out);
  309.     binout.writeUtf8Z(outputDoc);
  310.     binout.close();
  311.     
  312.     // I can't explain it, but we need to trim 4 bytes off the front or else it includes random crap
  313.     var trunc = 4;
  314.     var instream = storage.newInputStream(trunc);
  315.     
  316.     // Pass the data to the main content listener
  317.     this.listener.onDataAvailable(this.channel, aContext, instream, 0, storage.length - trunc);
  318.  
  319.     this.listener.onStopRequest(this.channel, aContext, aStatusCode);
  320.   }
  321. };
  322.  
  323. // We only have one component to register
  324. var components = [JSONView];
  325.  
  326. // The actual hook into XPCOM
  327. function NSGetModule(compMgr, fileSpec) {
  328.   function postRegister() {
  329.      var catMgr = XPCOMUtils.categoryManager;
  330.      catMgr.addCategoryEntry('ext-to-type-mapping','json','application/json',true,true);  
  331.   }
  332.   
  333.   
  334.   function preUnregister() {
  335.      var catMgr = XPCOMUtils.categoryManager;
  336.      catMgr.addCategoryEntry('ext-to-type-mapping','json',true); 
  337.   }
  338.   
  339.   return XPCOMUtils.generateModule(components, postRegister, preUnregister);
  340. }
  341.